﻿/*
   Copyright 2011 Softisis

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using FOM.Impl;
using FOM.Interfaces;

namespace FOM.Utils
{
    /// <summary>
    /// Static methods for object considerations.
    /// </summary>
    /// <remarks>Copyright Softisis 2011 (France)</remarks>
    public static class ObjectHelper
    {
        /// <summary>
        /// Create  new instance of whatever.
        /// </summary>
        /// <param name="type">The type to instanciate</param>
        /// <param name="addparam">Additionnal params for special cases (madatory in these cases): 
        /// If type is string, the source string.
        /// If type is array, the source object (that will be used to determine array size).
        /// If type is valuetype, the value itself.</param>
        /// <returns></returns>
        public static object CreateNewInstanceFromType(Type type, object addparam)
        {
            // not the same according to the type)
            if (type.IsValueType)
                return addparam; // no instanciation needed with value types
            if (type.Equals(typeof(string))) // string support
                return new string(((string) addparam).ToCharArray());

            if (type.Equals(typeof(string)))
            {
                string tmp = addparam as string;
                return new string(tmp == null ? new char[0] : tmp.ToCharArray()); // new instance of the string
            }
            
            if (type.IsArray)
            {
                // get the udly type of array
                var underref = type.GetElementType();
                if (underref == null)
                    throw new Exception("Unable to discover the underlying type of array " + type);
                
                // array needs to know the size. So we analyse the source that will
                // be used to later fill it.
                int arraysize = addparam == null ? 0 : ObjectHelper.DetermineCollectionSize(addparam);
                return Array.CreateInstance(underref, arraysize);
            }

            return Activator.CreateInstance(type);
        }

        /// <summary>
        /// Try to compute the size of a collection, array, dictionnary, list, enumerable. It tries to use the
        /// native size of the object if possible. If not, uses an iteration over the object, 
        /// which can lead to perf issues or seeking pb on special implementations.
        /// </summary>
        /// <param name="value">The object as list, collection, HashSet, dictionanry, array, etc ...</param>
        /// <returns></returns>
        public static int DetermineCollectionSize(Object value)
        {
            ICollection icoll = value as ICollection;
            IEnumerable ienum = value as IEnumerable;

            int length = -1;
            if (icoll != null)
                length = icoll.Count;
            else if (ienum != null) // downgrade method ==> risks of perf and/or seeking errors
            {
                length = 0;
                foreach (var v in (IEnumerable) value)
                    length++;
            }
            else
            {
                throw new Exception("Unable to determine Array size for " + value.GetType());
            }

            return length;
        }

        /// <summary>
        /// Dig into an object to the property pointed by path. Possible to create objects if null.
        /// Use overriding definition to create null object that are in the target path.
        /// </summary>
        /// <param name="objToDig">Source object to dig into</param>
        /// <param name="objectType">Object type. used if object is null AND createIfNonExisting is true</param>
        /// <param name="path">The path pointing at the property</param>
        /// <param name="overridingTypingNodes">Overrding types in the path. Use to instanciate overriden types</param>
        /// <param name="createIfNotExisting">If true, null object are dynamically created. Otherwise, null is returned.</param>
        /// <returns>Info on digged object</returns>
        public static DiggedObjectInfos DigAndBuild(
            Object objToDig, 
            Type objectType,
            String[] path,
            Dictionary<IPathLink, Type> overridingTypingNodes,            
            bool createIfNotExisting)
        {
            object tochange = null; // Final object, which have the target property to set

            object nextone = objToDig; // Intermediate value pointing at next element

            PropertyInfo fitgt = null; // le field info for current property

            Object tgtExistingObject = null; // contains the target value pointed, even if null
            int i = 0;  // dept counter

            string pathProgress = null;

            // iterate over path elements
            foreach (string s in path)
            {
                if (pathProgress==null) // used to build current path progressin and look at the overridetypes pathes
                    pathProgress = s;
                else
                    pathProgress += "." + s;

                i++; // dept counter
                object previous = tochange;
                tochange = nextone;

                // this is the "iteself" case. this is the case when we map (anything)
                // on an object, and not a property (ex: root case)
                if (s == "")
                {
                    if (nextone == null && createIfNotExisting)
                        nextone = Activator.CreateInstance(objectType);

                    // leaf case ==> we return what we know
                    if (i == path.Length) 
                        return new DiggedObjectInfos(fitgt, previous, nextone);

                    // Not teh leaf? We stay at the same point
                    tochange = previous; // rollback one iteration
                    continue; // itslef, so go on
                }

                // get the property of the current target object using current element path
                fitgt = tochange.GetType().GetProperty(s);
                if (fitgt == null) // not found?  That's not sound good
                    return null;

                // Useless for last path element, because that the target to be replaced/copied
                if (i < path.Length)
                {
                    nextone = fitgt.GetValue(tochange, null);
                    if (nextone == null)
                    {
                        if (!createIfNotExisting) // if we don't ask for creation, return null
                            return null;

                        // if intermediate object does not exist, we build it
                        // We must look at the OverridingType pathes that may have beed defined
                        // for this element.
                        Type overtype = null;
                        if (overridingTypingNodes != null)
                        {
                            PathLink pl = new PathLink("", pathProgress);
                            if (!overridingTypingNodes.TryGetValue(pl, out overtype))
                                overtype = null;
                        }

                        // no override? Manage uninstanciable cases. If override is specified, and
                        // is not instanciable, well ... it will raise an exception. We don't wand
                        // to substitute to an explicite choice, even if wrong.
                        if (overtype==null)
                        {
                            // if interface or abstract, use the target probably given by "itself" pathes
                            if (fitgt.PropertyType.IsAbstract || fitgt.PropertyType.IsInterface)
                                if (i == path.Length - 1 && path[i] == "")
                                    overtype = objectType;
                        }
                        // create it!
                        nextone = Activator.CreateInstance(overtype ?? fitgt.PropertyType);
                        
                        // and set it in the target object tree
                        fitgt.SetValue(tochange, nextone, null);
                    }
                }
                else
                {
                    // The last one is the targeted object (even if null)
                    tgtExistingObject = fitgt.GetValue(tochange, null); 
                }
            }

            return new DiggedObjectInfos(fitgt, tochange, tgtExistingObject);
        }


        /// <summary>
        /// Get the value of a property from an object, according to a path. If path elemnt is "", it's ignored
        /// and the process goes on.
        /// </summary>
        /// <param name="src">Object source to extract property from</param>
        /// <param name="path">Path decomposition pointing to supposed property</param>
        /// <returns></returns>
        public static Object GetValue(Object src, String[] path)
        {
            if (src == null)
                return null;

            // cas de l'obkjet source lui-même
            if (path.Length == 1 && path[0] == "")
                return src;

            object toret = null;
            object oref = src;

            foreach (var s in path)
            {
                if (s=="") continue; // "iteself"
                PropertyInfo fi = oref.GetType().GetProperty(s);
                if (fi == null)
                    return null;

                toret = fi.GetValue(oref, null);
                oref = toret;
                if (oref == null)
                    break;
            }
            return toret;
        }

        /// <summary>
        /// Usefull mathod to collect all properties of a type. Can filter on value types only.
        /// </summary>
        /// <param name="typeSourceObj">The source type</param>
        /// <param name="onlyValueTypes">If true, only props pointing to values types (and string) are collected</param>
        /// <returns>An IEnumerable on the properties</returns>
        public static IEnumerable<PropertyInfo> CollectProperties(Type typeSourceObj, bool onlyValueTypes)
        {
            if (!onlyValueTypes)
                return typeSourceObj.GetProperties();

            List<PropertyInfo> ret=new List<PropertyInfo>();
            var tmp = typeSourceObj.GetProperties();
            foreach (var pi in tmp)
                if (pi.PropertyType.IsValueType || pi.PropertyType==typeof(string))
                    ret.Add(pi);

            return ret;
        }


        /// <summary>
        /// This copies the given properties from the source object to the target object.
        /// </summary>
        /// <param name="src">Source object. if null, nothing is done</param>
        /// <param name="tgt">target object. If null, nothing is done.</param>
        /// <param name="props">Properties to copy. Only those are copied.</param>
        /// <param name="excludeNodes">Node path to exclude. If null, no exclusion</param>
        /// <returns>The target object itself</returns>
        public static object FlatCopyProperties(object src, object tgt, PropertyInfo[] props, List<IPathLink> excludeNodes)
        {
            return tgt;
        }
    }
}
